<?php
/*****************************************************************************
Copyright © 2008 The Regents of the University of Nevada
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
   derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/

interface iXMLDataStore {
	function save();
}

// Globals for the usort() comparison functions; the alternatives would
// be even uglier, AFAIK...
$sort_asc = true; $group_by_collection = true;

class XMLDataStore implements iDataStore, iXMLDataStore {

	const DATA_DIR = 'xmldata';
	const DATA_FILE = 'data.xml';
	private $sxml;
	private static $instance;	// self object


	protected function __construct() {}
	public function __clone() {}
	public function __wakeup() {}


	public static function getInstance() {
		if (!self::$instance instanceof self) {
			self::$instance = new self;
		}
		return self::$instance;
	}


	public function getName() { return 'XML File'; }


	public function load() {
		if (!$this->sxml instanceof SimpleXMLElement) {
			if (!$this->sxml = @simplexml_load_file($_SERVER['DOCUMENT_ROOT'] . '/'
				. trim(Config::ISIS_FOLDER_ROOT, '/') . '/'
				. trim(self::DATA_DIR, '/') . '/' . trim(self::DATA_FILE, '/'),
				'SimpleXMLElement', LIBXML_NOCDATA)) {
				throw new XMLDataStoreException(String::LOAD_DATA_XML_FAILED);
			}
		}
		return $this->sxml;
	}


	public function save() {
		$result = @$this->sxml->asXML($_SERVER['DOCUMENT_ROOT'] . '/'
			. trim(Config::ISIS_FOLDER_ROOT, '/') . '/'
			. trim(self::DATA_DIR, '/') . '/' . trim(self::DATA_FILE, '/'));
		if (!$result) throw new XMLDataStoreException(String::SAVE_DATA_XML_FAILED);
		return $result;
	}


	/***************************** COLLECTION *******************************/

	public function getAllCollections() {
		return $this->getCollectionObjectsFromXPath(
			Query::ALL_COLLECTIONS_XPATH);
	}


	public final function getAllCollectionsAsArray() {
		$result = $this->load()->xpath(Query::ALL_COLLECTIONS_XPATH);
		$cols = array();
		foreach ($result as $r) {
			$c['id'] = (int) $r->id;
			$c['alias'] = (string) $r->contentdm->alias;
			$c['oai_id_prefix'] = (string) $r->contentdm->oai_id_prefix;
			$c['path_to_oai'] = (string) $r->contentdm->path_to_oai;
			$c['path_to_thumbnail'] = (string) $r->contentdm->path_to_thumbnail;
			$c['name'] = (string) $r->name;
			$c['base_url'] = (string) $r->contentdm->base_url;
			$c['intro_url'] = (string) $r->intro_url;
			$c['organization'] = (string) $r->organization->name;
			$c['org_url'] = (string) $r->organization->url;
			$cols[] = $c;
		}
		return $cols;
	}


	public final function getAllCollectionsOutside($lat_n, $lat_s, $long_e,
	$long_w) {
		$xpath = sprintf(Query::ALL_MAPS_OUTSIDE_XPATH,
			(float) $lat_n, (float) $lat_s, (float) $long_e, (float) $long_w
		);
		$result = $this->getMapObjectsFromXPath($xpath);
		return $this->getUniqueCollectionsFromMapArray($result);
	}


	public final function getAllCollectionsPartiallyWithin($lat_n, $lat_s,
	$long_e, $long_w) {
		$xpath = sprintf(Query::ALL_MAPS_PARTIALLY_WITHIN_XPATH,
			(float) $long_w, (float) $long_e, (float) $long_e, (float) $long_w,
			(float) $lat_s, (float) $lat_n, (float) $lat_n, (float) $lat_s
		);
		$result = $this->getMapObjectsFromXPath($xpath);
		return $this->getUniqueCollectionsFromMapArray($result);
	}


	public final function getAllCollectionsWithin($lat_n, $lat_s, $long_e,
	$long_w) {
		$xpath = sprintf(Query::ALL_MAPS_WITHIN_XPATH,
			(float) $lat_s, (float) $long_w, (float) $lat_n, (float) $long_e
		);
		$result = $this->getMapObjectsFromXPath($xpath);
		return $this->getUniqueCollectionsFromMapArray($result);
	}


	private function getCollectionObjectsFromXPath($xpath) {
		$result = $this->load()->xpath($xpath);
		$cols = array();
		foreach ($result as $c) {
			$cols[] = new Collection((int) $c->id);
		}
		return $cols;
	}

	/* This currently doesn't work */
	public final function deleteCollection(Collection $c) {
		$xpath = sprintf(Query::GET_COLLECTION_PROPERTIES_XPATH, $c->getID());
		$result = $this->load()->xpath($xpath);
		unset($result[0]);
		return ($this->save()) ? true : false;
	}


	public final function loadCollectionProperties(Collection $c) {
		$xpath = sprintf(Query::GET_COLLECTION_PROPERTIES_XPATH,
			(int) $c->getID()
		);
		$result = $this->load()->xpath($xpath);
		$c->setName((string) $result[0]->name);
		$c->setIntroURL((string) $result[0]->intro_url);
		$c->setBaseURL((string) $result[0]->contentdm->base_url);
		$c->setAlias((string) $result[0]->contentdm->alias);
		$c->setPathToOAI((string) $result[0]->contentdm->path_to_oai);
		$c->setPathToThumbnail((string) $result[0]->contentdm->path_to_thumbnail);
		$c->setOAIIDPrefix((string) $result[0]->contentdm->oai_id_prefix);
		$c->setOrganization((string) $result[0]->organization->name);
		$c->setOrganizationURL((string) $result[0]->organization->url);
	}


	public final function saveCollection(Collection $c) {
		if (!$this->collectionHasSiblingWithSameNameAndURL($c)) { // insert
			$result = $this->load()->xpath('collections'); // get collections node
			$col = $result[0]->addChild('collection');
			$col->addChild('id',
				String::paranoid($this->getNextAvailableCollectionID()));
			$col->addChild('name', String::xmlentities($c->getName()));
			$col->addChild('intro_url', String::xmlentities($c->getIntroURL()));
			$cdm = $col->addChild('contentdm');
			$cdm->addChild('alias', String::xmlentities($c->getAlias()));
			$cdm->addChild('base_url', String::xmlentities($c->getBaseURL()));
			$cdm->addChild('oai_id_prefix',
				String::xmlentities($c->getOAIIDPrefix()));
			$cdm->addChild('path_to_oai', String::xmlentities($c->getPathToOAI()));
			$cdm->addChild('path_to_thumbnail',
				String::xmlentities($c->getPathToThumbnail()));
			$org = $col->addChild('organization');
			$org->addChild('name', String::xmlentities($c->getOrganization()));
			$org->addChild('url', String::xmlentities($c->getOrganizationURL()));
		}
		else { // update
			 // get existing collection node
			$xpath = sprintf(Query::GET_COLLECTION_PROPERTIES_XPATH, $c->getID());
			$sxml = $this->load()->xpath($xpath);
			$sxml[0]->id = String::paranoid($c->getID());
			$sxml[0]->name = String::xmlentities($c->getName());
			$sxml[0]->intro_url = String::xmlentities($c->getIntroURL());
			$sxml[0]->contentdm->alias = String::xmlentities($c->getAlias());
			$sxml[0]->contentdm->base_url = String::xmlentities($c->getBaseURL());
			$sxml[0]->contentdm->oai_id_prefix
				= String::xmlentities($c->getOAIIDPrefix());
			$sxml[0]->contentdm->path_to_oai
				= String::xmlentities($c->getPathToOAI());
			$sxml[0]->contentdm->path_to_thumbnail
				= String::xmlentities($c->getPathToThumbnail());
			$sxml[0]->organization->name
				= String::xmlentities($c->getOrganization());
			$sxml[0]->organization->url
				= String::xmlentities($c->getOrganizationURL());
		}
		return ($this->save()) ? true : false;
	}


	private function getNextAvailableCollectionID() {
		$result = $this->load()->xpath(Query::GET_COLLECTION_IDS_XPATH);
		$ids = array();
		foreach ($result as $r) {
			$ids[] = (int) $r[0];
		}
		return max($ids) + 1;
	}


	private function getUniqueCollectionsFromMapArray(array $maps) {
		$cids = array();
		foreach ($maps as $m) {
			$cids[] = (int) $m->getCollectionID();
		}
		$cids = array_unique($cids);
		$cols = array();
		foreach ($cids as $id) {
			$cols[] = new Collection($id);
		}
		return $cols;
	}


	public final function collectionHasSiblingWithSameID(Collection $c) {
		$xpath = sprintf(Query::GET_COLLECTION_PROPERTIES_XPATH,
			(int) $c->getID()
		);
		return (sizeof($this->load()->xpath($xpath)) > 0) ? true : false;
	}


	public final function collectionHasSiblingWithSameNameAndURL(Collection $c) {
		$xpath = sprintf(Query::GET_COLLECTION_PROPERTIES_XPATH,
			(int) $c->getName(), (string) $c->getBaseURL()
		);
		return (sizeof($this->load()->xpath($xpath)) > 0) ? true : false;
	}


	/******************************** MAPS ***********************************/

	public final function getAllMaps($page, $results_per_page, $sort,
	$sort_order) {

		global $sort_asc; // usort() needs this
		$sort_asc = ($sort_order == 'asc') ? true : false;

		$result = $this->getMapObjectsFromXPath(Query::ALL_MAPS_XPATH);
		$offset = ($page - 1) * $results_per_page;
		usort($result, array('XMLDataStore',
			self::getComparisonFunction($sort)));
		return array_slice($result, $offset, $results_per_page);
	}


	public final function getAllMapsAsArray() {
		$result = $this->load()->xpath(Query::ALL_MAPS_XPATH);
		$maps = array();
		foreach ($result as $m) {
			$map['id'] = (int) $m->id;
			$map['collection_id'] = (int) $m->collection_id;
			$map['ptr'] = (int) $m->ptr;
			$map['lat_n'] = (float) $m->lat_n;
			$map['lat_s'] = (float) $m->lat_s;
			$map['long_e'] = (float) $m->long_e;
			$map['long_w'] = (float) $m->long_w;
			$maps[] = $map;
		}
		return $maps;
	}


	public final function getAllMapsOutside($lat_n, $lat_s, $long_e, $long_w,
	$page, $results_per_page, $sort, $sort_order, $group_by_collection,
	array $collection_ids) {

		global $sort_asc, $group_by_collection; // usort() needs this
		$sort_asc = ($sort_order == 'asc') ? true : false;
		$group = ($group_by_collection) ? true : false;

		$xpath = sprintf(Query::ALL_MAPS_OUTSIDE_XPATH,
			(float) $lat_n, (float) $lat_s, (float) $long_e, (float) $long_w
		);
		$result = $this->getMapObjectsFromXPath($xpath);
		$offset = ($page - 1) * $results_per_page;
		usort($result, array('XMLDataStore', self::getComparisonFunction($sort)));
		if (count($collection_ids) > 0) {
			$count = count($result);
			for ($i = 0; $i < $count; $i++) {
				if (!in_array($result[$i]->getCollectionID(), $collection_ids)) {
					unset($result[$i]);
				}
			}
		}
		return array_slice($result, $offset, $results_per_page);
	}


	public final function getAllMapsPartiallyWithin($lat_n, $lat_s, $long_e,
	$long_w, $page, $results_per_page, $sort, $sort_order,
	$group_by_collection, array $collection_ids) {

		global $sort_asc, $group_by_collection; // usort() needs this
		$sort_asc = ($sort_order == 'asc') ? true : false;
		$group = ($group_by_collection) ? true : false;

		$xpath = sprintf(Query::ALL_MAPS_PARTIALLY_WITHIN_XPATH,
			(float) $long_w, (float) $long_e, (float) $long_e, (float) $long_w,
			(float) $lat_s, (float) $lat_n, (float) $lat_n, (float) $lat_s
		);
		$result = $this->getMapObjectsFromXPath($xpath);
		$offset = ($page - 1) * $results_per_page;
		usort($result, array('XMLDataStore', self::getComparisonFunction($sort)));
		if (count($collection_ids) > 0) {
			$count = count($result);
			for ($i = 0; $i < $count; $i++) {
				if (!in_array($result[$i]->getCollectionID(), $collection_ids)) {
					unset($result[$i]);
				}
			}
		}
		return array_slice($result, $offset, $results_per_page);
	}


	public final function getAllMapsWithin($lat_n, $lat_s, $long_e, $long_w,
	$page, $results_per_page, $sort, $sort_order, $group_by_collection,
	array $collection_ids) {

		global $sort_asc, $group_by_collection; // usort() needs this
		$sort_asc = ($sort_order == 'asc') ? true : false;
		$group = ($group_by_collection) ? true : false;

		$xpath = sprintf(Query::ALL_MAPS_WITHIN_XPATH,
			(float) $lat_s, (float) $long_w, (float) $lat_n, (float) $long_e
		);
		$result = $this->getMapObjectsFromXPath($xpath);
		$offset = ($page - 1) * $results_per_page;
		usort($result, array('XMLDataStore', self::getComparisonFunction($sort)));
		if (count($collection_ids) > 0) {
			$count = count($result);
			for ($i = 0; $i < $count; $i++) {
				if (!in_array($result[$i]->getCollectionID(), $collection_ids)) {
					unset($result[$i]);
				}
			}
		}
		return array_slice($result, $offset, $results_per_page);
	}


	private static function areaCompare(Map $a, Map $b) {
		global $sort_asc, $group_by_collection;
		$area_a = ($a->getLatN() - $a->getLatS())
			* ($a->getLongE() - $a->getLongW());
		$area_b = ($b->getLatN() - $b->getLatS())
			* ($b->getLongE() - $b->getLongW());
		$cid_a = $a->getCollectionID();
		$cid_b = $b->getCollectionID();

		if ($cid_a == $cid_b) {
			if ($area_a == $area_b) {
				return 0;
			}
			if ($sort_asc) {
				return ($area_a < $area_b) ? -1 : 1;
			}
			else return ($area_a > $area_b) ? -1 : 1;
		}
		else {
			return ($cid_a < $cid_b) ? -1 : 1;
		}
	}


	private static function centerLatCompare(Map $a, Map $b) {
		global $sort_asc, $group_by_collection;
		$clat_a = $a->getLatS() + ($a->getLatN() - $a->getLatS()) * 0.5;
		$clat_b = $b->getLatS() + ($b->getLatN() - $b->getLatS()) * 0.5;
		$cid_a = $a->getCollectionID();
		$cid_b = $b->getCollectionID();

		if ($cid_a == $cid_b) {
			if ($clat_a == $clat_b) {
				return 0;
			}
			if ($sort_asc) {
				return ($clat_a < $clat_b) ? -1 : 1;
			}
			else return ($clat_a > $clat_b) ? -1 : 1;
		}
		else {
			return ($cid_a < $cid_b) ? -1 : 1;
		}
	}


	private static function centerLongCompare(Map $a, Map $b) {
		global $sort_asc, $group_by_collection;
		$clong_a = $a->getLongW() + ($a->getLongE() - $a->getLongW()) * 0.5;
		$clong_b = $b->getLongW() + ($b->getLongE() - $b->getLongW()) * 0.5;
		$cid_a = $a->getCollectionID();
		$cid_b = $b->getCollectionID();

		if ($cid_a == $cid_b) {
			if ($clong_a == $clong_b) {
				return 0;
			}
			if ($sort_asc) {
				return ($clong_a < $clong_b) ? -1 : 1;
			}
			else return ($clong_a > $clong_b) ? -1 : 1;
		}
		else {
			return ($cid_a < $cid_b) ? -1 : 1;
		}
	}


	private static function ptrCompare(Map $a, Map $b) {
		global $sort_asc, $group_by_collection;
		$pa = $a->getPtr();
		$pb = $b->getPtr();
		$cid_a = $a->getCollectionID();
		$cid_b = $b->getCollectionID();

		if ($cid_a == $cid_b) {
			if ($pa == $pb) {
				return 0;
			}
			if ($sort_asc) {
				return ($pa < $pb) ? -1 : 1;
			}
			else return ($pa > $pb) ? -1 : 1;
		}
		else {
			return ($cid_a < $cid_b) ? -1 : 1;
		}
	}


	private static function getComparisonFunction($sort) {
		switch ($sort) {
			case 'clat': return 'centerLatCompare'; break;
			case 'clong': return 'centerLongCompare'; break;
			case 'ptr': return 'ptrCompare'; break;
			default: return 'areaCompare'; break;
		}
	}


	public final function mapHasSiblingWithSameCollectionAndPtr(Map $m) {
		$xpath = sprintf(Query::MAP_COL_PTR_EXISTS_XPATH,
			(int) $m->getPtr(), (int) $m->getCollectionID()
		);
		return (sizeof($this->load()->xpath($xpath)) > 0) ? true : false;
	}


	public final function mapHasSiblingWithSameID(Map $m) {
		$xpath = sprintf(Query::MAP_ID_EXISTS_XPATH, (int) $m->getID());
		return (sizeof($this->load()->xpath($xpath)) > 0) ? true : false;
	}


	/* This currently doesn't work */
	public final function deleteMap(Map $m) {
		$xpath = sprintf(Query::GET_MAP_PROPERTIES_XPATH, $m->getID());
		$result = $this->load()->xpath($xpath);
		unset($result);
		return ($this->save()) ? true : false;
	}


	public final function saveMap(Map $m) {
		if (!$this->mapHasSiblingWithSameCollectionAndPtr($m)) { // insert
			$result = $this->load()->xpath('maps'); // get maps node
			$map = $result[0]->addChild('map');
			$map->addChild('id', String::paranoid($this->getNextAvailableMapID()));
			$map->addChild('collection_id', String::paranoid($m->getCollectionID()));
			$map->addChild('ptr', String::paranoid($m->getPtr()));
			$map->addChild('lat_n', String::xmlentities($m->getLatN()));
			$map->addChild('lat_s', String::xmlentities($m->getLatS()));
			$map->addChild('long_e', String::xmlentities($m->getLongE()));
			$map->addChild('long_w', String::xmlentities($m->getLongW()));
		}
		else { // update
			 // get existing map node
			$xpath = sprintf(Query::GET_MAP_PROPERTIES_XPATH, $m->getID());
			$sxml = $this->load()->xpath($xpath);
			$sxml[0]->collection_id = String::paranoid($m->getCollectionID());
			$sxml[0]->ptr = String::paranoid($m->getPtr());
			$sxml[0]->lat_n = String::xmlentities($m->getLatN());
			$sxml[0]->lat_s = String::xmlentities($m->getLatS());
			$sxml[0]->long_e = String::xmlentities($m->getLongE());
			$sxml[0]->long_w = String::xmlentities($m->getLongW());
		}
		return ($this->save()) ? true : false;
	}


	private function getMapObjectsFromXPath($xpath) {
		$result = $this->load()->xpath($xpath);
		$maps = array();
		foreach ($result as $r) {
			$maps[] = new Map((int) $r->id);
		}
		return $maps;
	}


	public final function loadMapProperties(Map $m) {
		$xpath = sprintf(Query::GET_MAP_PROPERTIES_XPATH, $m->getID());
		$result = $this->load()->xpath($xpath);
		$m->setPtr((int) $result[0]->ptr);
		$m->setCollectionID((int) $result[0]->collection_id);
		$m->setLatN((float) $result[0]->lat_n);
		$m->setLatS((float) $result[0]->lat_s);
		$m->setLongE((float) $result[0]->long_e);
		$m->setLongW((float) $result[0]->long_w);
	}


	private function getNextAvailableMapID() {
		$result = $this->load()->xpath(Query::GET_MAP_IDS_XPATH);
		$ids = array();
		foreach ($result as $r) {
			$ids[] = (int) $r[0];
		}
		return max($ids) + 1;
	}


	public final function getNumMaps() {
		return count($this->load()->xpath(Query::ALL_MAPS_XPATH));
	}


	public final function getNumMapsOutside($lat_n, $lat_s, $long_e, $long_w,
	array $collection_ids) {
		$xpath = sprintf(Query::ALL_MAPS_OUTSIDE_XPATH,
			(float) $lat_n, (float) $lat_s, (float) $long_e, (float) $long_w
		);
		$result = $this->load()->xpath($xpath);
		$count = count($result);
		for ($i = 0; $i < $count; $i++) {
			if (!in_array((int) $result[$i]->collection_id, $collection_ids)) {
				unset($result[$i]);
			}
		}
		return count($result);
	}


	public final function getNumMapsPartiallyWithin($lat_n, $lat_s, $long_e,
	$long_w, array $collection_ids) {
		$xpath = sprintf(Query::ALL_MAPS_PARTIALLY_WITHIN_XPATH,
			(float) $long_w, (float) $long_e, (float) $long_e, (float) $long_w,
			(float) $lat_s, (float) $lat_n, (float) $lat_n, (float) $lat_s
		);
		$result = $this->load()->xpath($xpath);
		$count = count($result);
		for ($i = 0; $i < $count; $i++) {
			if (!in_array((int) $result[$i]->collection_id, $collection_ids)) {
				unset($result[$i]);
			}
		}
		return count($result);
	}


	public final function getNumMapsWithin($lat_n, $lat_s, $long_e, $long_w,
	array $collection_ids) {
		$xpath = sprintf(Query::ALL_MAPS_WITHIN_XPATH,
			(float) $lat_s, (float) $long_w, (float) $lat_n, (float) $long_e
		);
		$result = $this->load()->xpath($xpath);
		$count = count($result);
		for ($i = 0; $i < $count; $i++) {
			if (!in_array((int) $result[$i]->collection_id,
			$collection_ids)) {
				unset($result[$i]);
			}
		}
		return count($result);
	}

} // XMLDataStore

?>
